﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;
using Newtonsoft.Json;
using WebMatrix.WebData;
using G28BS.Models;
using G28BS.Code;

namespace G28BS.Hubs
{
    [Authorize]
    public static class UserHandler
    {
        // Dictionary that stores all the connectionID's of all the users.
        // Since each User has multiple ConnectionID's for each tab he has open we keep the ConnectionID's for each User as a list
        // To get all the connectionID's for a specific user you can do:
        // var asdf = UserHandler.ConnectedIds[idOfUserHere];
        public static Dictionary<int, List<string>> ConnectedIds = new Dictionary<int, List<string>>();

    }

    public class MasterHub : Hub
    {
        private IAppRepository repo = null;
        private IUserService userService = null;

        public MasterHub()
        {
            repo = new AppRepository();
            userService = new AppUserService();
        }

        public MasterHub(IAppRepository r, IUserService u)
        {
            repo = r;
            userService = u;
        }

        // Overrides the OnConnected Signalr method. Everytime someone connects to Signalr we add his ConnectionID to the Dictionary
        public override Task OnConnected()
        {
            // Checks if the User already has connectionId's in the Dictionary.
            if (WebSecurity.IsAuthenticated)
            {
                if (UserHandler.ConnectedIds.ContainsKey(userService.GetUserID()))
                {
                    // Checks to make sure the connectionID we're about to add is not already in the list.
                    if (!UserHandler.ConnectedIds[userService.GetUserID()].Contains(Context.ConnectionId))
                        UserHandler.ConnectedIds[userService.GetUserID()].Add(Context.ConnectionId);
                }
                else
                {
                    // Since the User doesn't have a connectionID in the dictionary we create a new list for this User's connectionId's
                    UserHandler.ConnectedIds[userService.GetUserID()] = new List<string> { Context.ConnectionId };
                }
            }
            return base.OnConnected();
        }

        // Overrides the OnConnected Signalr method. Everytime someone disconnects from Signalr we remove his ConnectionID from the Dictionary
        public override Task OnDisconnected()
        {
            // Iterates through all the Users in the Dictionary
            foreach (int userID in UserHandler.ConnectedIds.Keys)
            {
                // Checks if the user owns this ConnectionId
                if (UserHandler.ConnectedIds[userID].Contains(Context.ConnectionId))
                {
                    // Remove the ConnectionId
                    UserHandler.ConnectedIds[userID].Remove(Context.ConnectionId);
                }
            }
            return base.OnDisconnected();
        }

        // Join method for clients to join a group.
        [Authorize]
        public void Join(string groupID)
        {
            Groups.Add(Context.ConnectionId, groupID);
        }

        // For chat. Sends the message to everyone in the group.
        [Authorize]
        public void SendMessage(string groupID, string message)
        {
            // Calls the addMessage method to update clients in group.
            Clients.Group(groupID).addMessage(Context.User.Identity.Name, message);
        }

        // For GameRoom. -----------------------------------------
        [Authorize]
        public void UpdateName(string groupID, int gameRoomID, string name)
        {
            GameRoom gameRoom = repo.gameRooms.FirstOrDefault(gr => gr.gameRoomID == gameRoomID);
            if (gameRoom != null)
            {
                // Update name on object and save to database.
                gameRoom.gameRoomName = name;
                repo.Save();

                // Calls the UpdateName method to update clients in group.
                Clients.Group(groupID).updateName(name);
            }
        }
        [Authorize]
        public void UpdateSlots(string groupID, string s, string grID)
        {
            // No database functions here. This only echoes slot array to clients.
            // Call the UpdateSlots method on gameRoom.
            Clients.Group(groupID).updateSlots(s);
            // And updatePlayers to notify game.
            Clients.Group(groupID).updatePlayers(s);
        }
        [Authorize]
        public void UpdateSlot(string groupID, string slotJson, string grID)
        {
            int gameRoomID;
            if (int.TryParse(grID, out gameRoomID))
            {
                var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
                Slot updatedSlot = serializer.Deserialize<Slot>(slotJson);
                var slot = (from s in repo.slots
                            where s.slotID == updatedSlot.slotID
                            select s).SingleOrDefault();
                if (slot != null)
                {
                    // Update each property by hand. Did not find an easy way to do multiple assignment.
                    slot.userProfileID = updatedSlot.userProfileID;
                    slot.hubConnectionID = updatedSlot.hubConnectionID;
                    slot.userName = updatedSlot.userName;
                    slot.occupied = updatedSlot.occupied;
                    slot.ready = updatedSlot.ready;
                    slot.isCPU = updatedSlot.isCPU;
                    slot.userImage = updatedSlot.userImage;
                    repo.Save();
                }

                var slots = (from s in repo.slots
                             where s.gameRoomID == gameRoomID
                             select s);
                string slotsJson = serializer.Serialize(slots);
                // Call the UpdateSlots method on gameRoom.
                Clients.Group(groupID).updateSlots(slotsJson);
                // And updatePlayers to notify game.
                Clients.Group(groupID).updatePlayers(slotsJson);
            }
        }

        // This method only handles setting gameroom state.
        // Subclasses should usually override this method but always call _UpdateGameRoomState() for updating database.
        [Authorize]
        public virtual void StartGame(string groupID, int gameRoomID)
        {
            try
            {
                _UpdateGameRoomState(gameRoomID, GameRoomStates.Active);
            }
            catch (KeyNotFoundException e)
            {
                Clients.Group(groupID).error(e.Message);
            }
            // Notify clients in group about state change.
            Clients.Group(groupID).updateState(GameRoomStates.Active.ToString());
            Clients.Group(groupID).startGame();
        }

        // Update database after game finished.
        [Authorize]
        public virtual void EndGame(string groupID, int gameRoomID)
        {
            try
            {
                // Register game completion.
                _EndOfGameUpdateRecords( groupID, gameRoomID);

                _UpdateGameRoomState(gameRoomID, GameRoomStates.Finished);
            }
            catch (KeyNotFoundException e)
            {
                Clients.Group(groupID).error(e.Message);
            }
            // Notify clients in group about state change.
            Clients.Group(groupID).updateState(GameRoomStates.Finished.ToString());
        }

        protected void _UpdateGameRoomState(int gameRoomID, GameRoomStates state)
        {
            GameRoom gameRoom = repo.gameRooms.FirstOrDefault(gr => gr.gameRoomID == gameRoomID);
            if (gameRoom != null)
            {
                // Update name on object and save to database.
                gameRoom.state = state;
                repo.Save();
            }
            else
            {
                throw new KeyNotFoundException("GameRoom could not be loaded! Database error.");
            }
        }

        [Authorize]
        public void GetLoggedInUsers()
        { 
            //var loggedIn = UserHandler.ConnectedIds.Count();
            List<UserProfile> loggedInUsers = new List<UserProfile>();
            
            //var connectedUsers = (from x in UserHandler.ConnectedIds select x.Value).ToList().Distinct();
            var connectedUsers = UserHandler.ConnectedIds.Keys.AsEnumerable();

            foreach (var y in connectedUsers)
            {
                //loggedInUsers.Add((from x in repo.userProfiles where x.UserId = y select x).ToList().Distinct());
            }
            
            //var xixi = (from x in repo.userProfiles where x.UserId  = Convert.ToInt32(connectedUsers)  select x).ToList().Distinct();
            
            //loggedInUsers.Add( (from x in repo.userProfiles where x.UserName UserHandler.ConnectedIds.Values select x).ToList().Distinct() );

            //for(var r = 0; r < connectedUsers.Count() ; r++)
            //{
            //loggedInUsers.Add((from x in repo.userProfiles where x.UserId == Convert.ToInt32(UserHandler.ConnectedIds.Keys) select x).ToList().Distinct());
                //loggedInUsers.Add(repo.userProfiles.
            //}
        }

        // Signalr method for invitation to gamerooms by userid.
        [Authorize]
        public void InviteId(string gameRoomID, string inviteName, string inviteeID)
        {
            int inviteeUserId = Convert.ToInt32(inviteeID);

            if (UserHandler.ConnectedIds.ContainsKey(inviteeUserId))
            {
                // Gets a list of all the connections of the user's id.
                var inviteeConnections = UserHandler.ConnectedIds[inviteeUserId];
                // Itaretes through the list of connections of the user.
                foreach (var connection in inviteeConnections)
                {
                    // Sends the invite to all the connections of the user.
                    Clients.Client(connection).acceptInvite(gameRoomID, inviteName);
                }
            }
            return;
        }

        // Signalr method for invitation to gamerooms.
        [Authorize]
        public void InviteUser(string gameRoomID, string inviteName, string inviteeName)
        {
            // Gets the UserId of the invitee.
            int userId = WebSecurity.GetUserId(inviteeName);

            if (UserHandler.ConnectedIds.ContainsKey(userId))
            {
                // Gets a list of all the connections of the user's id.
                var inviteeConnections = UserHandler.ConnectedIds[userId];
                // Itaretes through the list of connections of the user.
                foreach (var connection in inviteeConnections)
                {
                    // Sends the invite to all the connections of the user.
                    Clients.Client(connection).acceptInvite(gameRoomID, inviteName);
                }
            }
            return;
        }

        /* For retrieving match history */
        /* Returns a number of recent games with player of playerId.
         * If only numberOf is specified, the function will return a
         * number of most recent matches across all users.
         * If no argument is given to this function, it will return all
         * games from history. */
        public void GetMatchHistory(int numberOf, int playerId)
        {
            IEnumerable<FinishedGame> games = null;
            if (playerId <= 0)
            {
                // Select all games for any userId as player.
                games = (from game in repo.finishedGames
                         orderby game.gameEnded descending
                         select game).Take(numberOf);
            }
            else
            {
                // Select only games that have this userId as player.
                games = (from game in repo.finishedGames
                         orderby game.gameEnded descending
                         where (";" + game.playerUserIds + ";").Contains(";" + playerId + ";")
                         select game).Take(numberOf);
            }

            foreach (var game in games)
            {
                // Add profiles to the profile list in finished game objects.
                var uids = Array.ConvertAll(game.playerUserIds.Split(';').Where(x => x.Length > 0).Cast<string>().ToArray<string>(), int.Parse);
                game.playerUserProfiles = new List<UserProfile>();
                foreach (var uid in uids)
                {
                    var p = (from profile in repo.userProfiles
                             where profile.UserId == uid
                             select profile).SingleOrDefault();

                    game.playerUserProfiles.Add(p.SafeInformationCopy());
                }
                // Add game type to the game type variable in the finished game objects.
                game.gameType = (from type in repo.games
                                 where type.gameID == game.gameTypeGameID
                                 select new Game { gameID = type.gameID, name = type.name }).SingleOrDefault();
            }

            Clients.Caller.updateMatchHistory(games);
        }

        public void _EndOfGameUpdateRecords(string groupID, int gameRoomID)
        {
            // Search up game room.
            GameRoom gameRoom = (from g in repo.gameRooms 
                                 where g.gameRoomID == gameRoomID 
                                 select g).SingleOrDefault();
            // Search up game type.
            Game gameType = (from t in repo.games
                             where t.gameID == gameRoom.gameID 
                             select t).SingleOrDefault();
            // Search up user ids using slots.
            var userIds = (from s in repo.slots 
                           where s.gameRoomID == gameRoomID && s.isCPU == false 
                           select s.userProfileID).Take(gameType.maxPlayers).TakeWhile(x => x > 0);

            // Update repository records.
            // Update user profile.
            foreach (var uId in userIds)
            {
                (from up in repo.userProfiles 
                 where up.UserId == uId 
                 select up).SingleOrDefault().gameCount++;
            }
            // Update game history.
            string userIdString = "";
            foreach (var uId in userIds)
            {
                userIdString += (uId + ";");
            }
            repo.AddFinishedGame(new FinishedGame { playerUserIds = userIdString, gameTypeGameID = gameType.gameID, gameEnded = DateTime.Now });

            // save changes to database
            repo.Save();
        }
    }
}